查看原文
其他

研发日记 | 开源软件 Goreleaser X CGO 最佳实践是如何诞生的

Junyi Bytebase 2022-12-19


本篇比较详细地记录了 Bytebase 从遇到问题最后到如何使用 goreleaser X CGO 的经过。

背景

在实现 SQL Review for PG 时,我们引入了 pg_query_go 作为 PG parser。pg_query_go 通过 C Bind 的方式使用原生的 PG parser 编译,自然就需要 CGO 的支持。

在 1.2.1 版本发布时,我们发现 goreleaser 无法正常工作,其报错信息直指 pg_query_go。幸运的是,我们的发布版本不需要使用 pg_query_go, 因此先通过添加 ”!release“ 的 Go tag 来保证正常发布,之后就开始了与 goreleaser 和 CGO 斗争的漫漫长路上。

线索一

“build constraints exclude all Go files in /go/pkg/mod/github.com/pganalyze/pg_query_go/v2@2.1.2/parser”
这个错误信息虽然看起来有点莫名其妙,不过它精确指向了出问题的地方。那就让我们前往 “pg_query_go/parser” 一探究竟。
实际上,“pg_query_go/parser” 这个包里只有一个 Go 文件 parser.go,除此之外都是 C 代码。并且,parser.go 是一个 import “C” 的 CGO 文件。容易猜到,可能是没有设置 CGO_ENABLED=1 导致忽略了这个文件。
Google 或者手动验证一下我们都可以轻松验证这个猜想的正确性。由此我们获得了前往下一个路口的指引:在 goreleaser 中设置 CGO_ENABLED=1

线索二

在 goreleaser 中打开 CGO 只需要在 goreleaser 配置文件中将 “CGO_ENABLED=1” 加入到对应的 env 项中即可
让我们重试:
这个错误看起来就比较奇怪了,只能知道是 cgo 出了问题,莫非 goreleaser 不支持 CGO?抱着怀疑的心情前去 gorelaser 文档搜索了一下,果然如此。
goreleaser 中提到的「This project」实际上是 goreleaser/goreleaser-cross。那么,下一步我们就一起前往 goreleaser-cross 吧

线索三

goreleaser-cross 的内容并不复杂,这个仓库实际上是为了提供一个 docker image,这个 docker image 中主要包括的内容是 goreleaser 和一些 C/C++ 的交叉编译工具链(cross compiler)。
了解交叉编译的同学可能知道,交叉编译的使用场景是需要在平台A上编译能够直接执行在平台B上的二进制文件,例如,在嵌入式、操作系统等开发场景,我们的开发机器往往是 x64 架构的 Linux 环境,但运行环境可能是 arm/arm64 架构的 Linux 环境,此时就需要一个对应的交叉工具链来进行编译。
那为什么我们需要这个呢,仔细的同学可能已经发现,我们在 goreleaser 的配置文件中指定了四个目标平台:
而我们的编译环境实际上是在 Github Action 的 Ubuntu x64 上。在打开 CGO 之前,我们只需要处理跨平台编译 Go 时的参数即可,而这一步实际上 Go 语言编译器和 goreleaser 已经帮我们处理好了。但在引入 CGO 之后,我们还需要编译 C/C++ 代码,所以就需要对应的 C/C++ 交叉编译工具链了。
也就是说,此时我们需要一共四个目标平台的交叉编译工具链,幸运的是,goreleaser-cross 都可以支持。
现在看起来问题似乎得以解决,但实际上并不完美,仍然有一些问题,相较于 goreleaser 拥有 10k+ 的 stars,goreleaser-cross 只有 26 stars。这意味 goreleaser-cross 很少被使用,可能有一些风险。
如果交叉编译工具链都是良好维护的,那么这个风险实际上也比较小,主要的问题在于目标平台是 Darwin 时。尝试过从 Linux 交叉编译到 Darwin 上的同学都知道,这是一件极其困难的事情。主要困难的点在于没有现成的、良好维护的交叉编译工具链可以使用,往往需要自行构建交叉工具链。自行构建的交叉编译工具链又往往难以验证兼容性和维护,需要消耗大量精力。实际上,goreleaser-cross 使用的 o64-clang/oa64-clang 交叉编译工具链就是基于 osxcross 自行编译的。
那么是否有办法规避这个风险呢?确实有的。

分岔路

之前提到我们在 Github Action 上使用的编译环境是 Ubuntu x64。想要不使用交叉编译工具链,最简单的办法就是在相同平台上编译即可。那 Github Action 是否提供了其他环境呢?有的!
注意,这里的 macOS 都是 x64 架构的。虽然还是需要跨架构,但是好在不用跨平台了!
那么,接下来的思路就变成了:在 Linux 平台上编译 Linux 两个架构的 binary,在 macOS 平台上编译 Darwin 两个架构的 binary。
对于 Linux 平台来说,x64 -> x64 就是我们日常编译所使用的工具链 gcc/g++ 即可,x64 -> arm64 在 Linux 平台是有良好维护的工具链,对 Ubuntu 来讲,直接通过包管理器即可获得:
sudo apt-get -y install gcc-aarch64-linux-gnu
对于 Darwin 来说,就更简单了,clang 就原生支持跨架构,感谢 LLVM!
一番操作之后,Github Action 配置变成了这样:
两个 job 分别用来编译不同平台上的二进制文件。
对于 Darwin 平台的 goreleaser 配置文件,只需要修改打开 CGO 然后去掉 Linux 平台即可。
对于 Linux 平台的 goreleaser 配置文件,稍微复杂,我们需要给对应的架构使用对应的编译工具链。
这里使用了 goreleaser 的 overrides 功能来实现这个目的。
到此为止,我们已经能够使用 goreleaser 在 CGO 开启时进行编译了,但是故事还没有结束,goreleaser 的最终目的是发布,若是按照目前这样 release 两次就会有两次 release 信息。不过这一步就比较轻松了。

release!

这里的主要思路很简单,build 和 release 分离。在 job1 中 build linux binaries;在 job2 中 build macOS binaries;job3 依赖 job1 和 job2,进行发布。
build 时通过 skip-publish 参数跳过 release,而 release 时通过 build skip 来跳过 build,并且通过 extra_files 设置将 job1 和 job2 中生成的文件设置为发布内容。
故事完结!

一则趣事


实际上在此之前,Bytebase 已经依赖了 go-sqlite3,这也是一个依赖 CGO 的包,那为什么之前没出问题呢。实际上,在此之前 goreleaser 环境中并没有打开 CGO,而 go-sqlite3 在没打开 CGO 时做了一个 mock,也就是说,没打开 CGO 时编译出来的 go-sqlite3 是一个空包。

那为什么没有遇到问题呢,因为在引入 goreleaser 之前,Bytebase 已经从 从 SQLite 迁移到了 PostgreSQL, 所以确实没有使用 go-sqlite3, 只是有这个依赖而已。

后记

- 不到万不得已,不要尝试引入交叉编译工具链,他会带来额外的验证和维护成本。
- 跨架构会让事情变得复杂,而跨平台会让你变得不幸,尽可能远离它!!!

- 感谢 LLVM-Clang

贝斯的圆桌趴 |X as Code 万物代码化
关于数据库你应该知道的那些事儿
盘点十大 DevOps 刚需场景,给选型头秃的你一套标准开源工具集 (2022 年中版)
Bytebase 1.2.2 - 2022.7.21

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存